using System;
using System.Linq;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Type;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.NHibernate.DynamicQuery;
using TelerikMvcGridCustomBindingHelper.NHibernate.ORMIntegration;
using TelerikMvcGridCustomBindingHelper.NHibernate.Projections;
using TelerikMvcGridCustomBindingHelper.NHibernate.Util;
using TelerikMvcGridCustomBindingHelper.Projections;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.NHibernate
{
    public static class Extencions
    {
        #region public members

        #region QueryOver complex aggregates

        public static ExpressionProjectionHelper Aggregate<T>(this IQueryOver<T> queryover, Expression<Func<T, object>> expression, IType returnType = null)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType);
        }

        #endregion

        #region QueryOver simple aggregates

        public static IFutureValue<object> Sum<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression)
        {
            return Sum(queryover, expression, null);
        }

        public static IFutureValue<object> Sum<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression, IType returnType)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType).Sum();
        }

        public static IFutureValue<object> Count<T>(this IQueryOver<T> queryover, Expression<Func<T, bool>> expression)
        {
            return Count(queryover, expression, null);
        }

        public static IFutureValue<object> Count<T>(this IQueryOver<T> queryover, Expression<Func<T, bool>> expression, IType returnType)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType).Count();
        }

        public static IFutureValue<object> Average<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression)
        {
            return Average(queryover, expression, null);
        }

        public static IFutureValue<object> Average<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression, IType returnType)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType).Average();
        }

        public static IFutureValue<object> Min<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression)
        {
            return Min(queryover, expression, null);
        }

        public static IFutureValue<object> Min<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression, IType returnType)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType).Min();
        }

        public static IFutureValue<object> Max<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression)
        {
            return Max(queryover, expression, null);
        }

        public static IFutureValue<object> Max<T>(this IQueryOver<T> queryover, Expression<Func<T, decimal?>> expression, IType returnType)
        {
            return new ExpressionProjectionHelper(queryover, expression, returnType).Max();
        }

        #endregion

        #region NHibernate integration

        public static IFluentORMIntegrationConfiguration StartNHibernateIntegration<TNHibernateIntegration>(this IFluentMapperConfiguration configuration)
            where TNHibernateIntegration : class, INHibernateIntegration, new()
        {
            return GridModelMapper.Configuration.StartORMIntegration<TNHibernateIntegration>();
        }

        public static IFluentORMIntegrationConfiguration StartNHibernateIntegration(this IFluentMapperConfiguration configuration, ISessionFactory sessionFactory)
        {
            Contract.Expect(() => sessionFactory).IsNotNull();

            return GridModelMapper.Configuration.StartORMIntegration(() => new NHibernateIntegration(sessionFactory));
        }

        #endregion

        #endregion

        #region Projections

        public static IProjectionExpression<TSource> Project<TSource>(this IQueryOver<TSource, TSource> source, Action<ProjectionsOptions<TSource>> options = null) where TSource : class
        {
            if (options != null)
            {
                var projectionsOptions = new ProjectionsOptionsImpl<TSource>();
                options.Invoke(projectionsOptions);
                return new Projections.ProjectionExpression<TSource>(source, projectionsOptions);
            }

            return new Projections.ProjectionExpression<TSource>(source, null);
        }

        internal static IProjectionExpression<TSource> Project<TSource>(this IQueryOver<TSource, TSource> source, ProjectionsOptionsImpl<TSource> options) where TSource : class
        {
            return new Projections.ProjectionExpression<TSource>(source, options);
        }

        #endregion

        #region internal members

        internal static void CreateCriteria(this IQueryOver queryOver, Alias alias)
        {
            Contract.Expect(() => queryOver).IsNotNull();
            Contract.Expect(() => alias).IsNotNull();

            var criteria = queryOver.RootCriteria.GetCriteriaByPath(alias.AssociationPath);
            criteria = criteria ?? queryOver.RootCriteria.GetCriteriaByAlias(alias.AssociationPath);
            criteria = criteria ?? queryOver.RootCriteria.GetCriteriaByAlias(alias.TheAlias);

            if (criteria == null)
            {
                queryOver.RootCriteria.CreateCriteria(alias.AssociationPath, alias.AssociationPath);
            }
        }

        internal static IQueryOver<TEntity, TEntity> Where<TEntity, TViewModel>(this IQueryOver<TEntity, TEntity> queryOver, QueryOverDynamicWhereClause<TEntity, TViewModel> dynamicWhereClause)
            where TEntity : class
            where TViewModel : class
        {
            var expression = dynamicWhereClause.Predicate;
            var targetProperty = dynamicWhereClause.TargetProperty;

            if (targetProperty.IsNotNullOrWhiteSpace() && targetProperty.Contains("."))
            {
                var criteriaAlias = Generator.GetCriteriaAliasFor(dynamicWhereClause.TargetProperty);
                queryOver.CreateCriteria(criteriaAlias);
            }
            else if (dynamicWhereClause.GridPropertyInfo.CustomExpression != null)
            {
                var propertiesPaths = dynamicWhereClause.GridPropertyInfo.CustomExpression.GetAllPropertiesPaths();

                foreach (var alias in propertiesPaths.Where(s => s.Contains('.')).Select(Generator.GetCriteriaAliasFor))
                {
                    queryOver.CreateCriteria(alias);
                }
            }

            queryOver.RootCriteria.Add(expression);

            return queryOver;
        }

        #endregion

        #region Expression extensions

        public static string ToSqlString(this Expression expression, bool isConditionalExpression = false)
        {
            Guard.IsNotNull(() => expression);

            isConditionalExpression = isConditionalExpression || IsConditionalExpression(expression);

            switch (expression.NodeType)
            {
                case ExpressionType.Add:
                    var add = (BinaryExpression)expression;
                    return String.Format("{0} + {1}", add.Left.ToSqlString(isConditionalExpression),
                                         add.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.Subtract:
                    var subtract = (BinaryExpression)expression;
                    return String.Format("{0} - {1}", subtract.Left.ToSqlString(isConditionalExpression),
                                         subtract.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.Multiply:
                    var multiply = (BinaryExpression)expression;
                    return String.Format("{0} * {1}", multiply.Left.ToSqlString(isConditionalExpression),
                                         multiply.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.Divide:
                    var divide = (BinaryExpression)expression;
                    return String.Format("{0} / {1}", divide.Left.ToSqlString(isConditionalExpression),
                                         divide.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.GreaterThan:
                    var greaterThan = (BinaryExpression)expression;
                    return String.Format("{0} > {1}", greaterThan.Left.ToSqlString(isConditionalExpression),
                                         greaterThan.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.GreaterThanOrEqual:
                    var greaterThanOrEqual = (BinaryExpression)expression;
                    return String.Format("{0} >= {1}", greaterThanOrEqual.Left.ToSqlString(isConditionalExpression),
                                         greaterThanOrEqual.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.LessThan:
                    var lessThan = (BinaryExpression)expression;
                    return String.Format("{0} < {1}", lessThan.Left.ToSqlString(isConditionalExpression),
                                         lessThan.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.LessThanOrEqual:
                    var lessThanOrEqual = (BinaryExpression)expression;
                    return String.Format("{0} <= {1}", lessThanOrEqual.Left.ToSqlString(isConditionalExpression),
                                         lessThanOrEqual.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.Equal:
                    var equal = (BinaryExpression)expression;
                    return String.Format("{0} = {1}", equal.Left.ToSqlString(isConditionalExpression),
                                         equal.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.NotEqual:
                    var notEqual = (BinaryExpression)expression;
                    return String.Format("{0} <> {1}", notEqual.Left.ToSqlString(isConditionalExpression),
                                         notEqual.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.AndAlso:
                    var andalso = (BinaryExpression)expression;
                    return String.Format("{0} and {1}", andalso.Left.ToSqlString(isConditionalExpression),
                                         andalso.Right.ToSqlString(isConditionalExpression));

                case ExpressionType.Constant:
                    var constant = (ConstantExpression)expression;
                    return constant.Type == typeof(string)
                               ? String.Format("'{0}'", constant.Value.ToString().Replace("'", "''"))
                               : constant.Value.ToString();

                case ExpressionType.Lambda:
                    var lambdaExpression = (LambdaExpression)expression;
                    return lambdaExpression.Body.ToSqlString(isConditionalExpression);

                case ExpressionType.MemberAccess:
                    var visitor = new PropertyPathVisitor();
                    visitor.Visit(expression);
                    var propertyPath = visitor.Properties.ElementAt(0);
                    return String.Format("{{{0}}}", propertyPath);

                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression)expression;
                    return unaryExpression.Operand.ToSqlString(isConditionalExpression);
            }

            throw new NotSupportedException(String.Format("{0} {1}", expression.GetType(), expression.NodeType));
        }

        private static bool IsConditionalExpression(Expression expression)
        {
            var conditionalVisitor = new ConditionalVisitor();
            conditionalVisitor.Visit(expression);
            return conditionalVisitor.HasCondition;
        }

        private class ConditionalVisitor : ExpressionVisitor
        {
            public bool HasCondition { get; private set; }

            public override Expression Visit(Expression expression)
            {
                switch (expression.NodeType)
                {
                    case ExpressionType.GreaterThan:
                        HasCondition = true;
                        break;
                    case ExpressionType.GreaterThanOrEqual:
                        HasCondition = true;
                        break;

                    case ExpressionType.LessThan:
                        HasCondition = true;
                        break;

                    case ExpressionType.LessThanOrEqual:
                        HasCondition = true;
                        break;

                    case ExpressionType.Equal:
                        HasCondition = true;
                        break;

                    case ExpressionType.NotEqual:
                        HasCondition = true;
                        break;

                    case ExpressionType.AndAlso:
                        HasCondition = true;
                        break;
                }
                return base.Visit(expression);
            }
        }

        #endregion
    }
}